<?php
/**
 * db.class
 * 
 * Singleton database wrapper
 * 
 * @version:	0.5
 * @package:	core
 * @author:	alex_ez
 * @created:	11 nov 2008
 *
 * @link:	http://src.qfox.ru/
 *
 * @copyright:	(c) 2008 flamebox.ru. All rights reserved.
 */

class db extends singleton
{
	// php singleton wrapper
	static public function &instance() { return parent::instance(__CLASS__); }
	
	static private $link = null;
	
	static private $stats = null;
	
	private $last_query = null;
	/**
	 * Конструктор
	 *
	 * @access protected
	 */
	protected function __construct( )
	{
		if( !defined( 'DB_URL' ) ) return;
		
		$z = parse_url( DB_URL );
		if( $z['scheme'] !== 'mysqli' )
			return null;
		
		list(, $_db ) = explode( '/', $z['path'] );
		
		$_params = array( 'charset' => 'utf8', );
		parse_str( $z['query'], $_query ); $z['query'] = $_query; unset( $_query );
		foreach( $z['query'] as $k => $v ) $_params[ $k ] = $v;
		
		self::$link = new mysqli(
			$z['host'],
			$z['user'],
			$z['pass'],
			$_db,
			$z['port']
			);
		
		if( $_e = mysqli_connect_errno() ) {
			throw new Exception( sprintf( "Connect failed: %s\n", $_e ) );
		}
                
		if( !is_array( self::$stats ) )
			self::$stats = array( 'queries_Count' => 0, 'queries_Time' => 0, 'execs_Count' => 0, 'execs_Time' => 0 );
		
		self::$link ->set_charset( $_params['charset'] );
	}
	
	/** 
	 * killer
	 *
	 * @access public
	 */
	public function __destruct( )
	{
		if( is_object( self::$link ) ) self::$link ->close( );
		self::$link = null;
	}



	/**
	 * mysqli methods wrapper
	 *
	 * @access public
	 * @param string $name имя метода
	 * @param array $params массив аргументов (в коде не нужно передавать массив)
	 * @return mixed
	 */
	public function __call( $name, $params )
	{
		if( method_exists( self::$link, $name ) )
			return call_user_func_array( array( self::$link, $name ), $params );
		
		throw new Exception( 'Method "'.$name.'" of class "'.__CLASS__.'" does not exists' );
	}

	/**
	 * accessing to properties of encapsulated mysqli-object
	 *
	 * @param	string	property name of encapsulated mysqli-object
	 * @return	mixed	value, if exists, or null
	 * @date	27 august 2008
	 * @author	alex yaroshevich
	 */
	public function __get( $property )
	{
		return self::$link ->$property;
	}
	
	public function exec( $q )
	{
		if (bc::enabled()) bc::group( 'exec['.self::$stats['execs_Count'].']'/*, array( 'Collapsed'=>false, 'Color' => '#941' )*/ );
		$_z = microtime(1);
		$res = self::$link ->query( $q );
		$this ->last_query = $q;
		if (bc::enabled()) bc::log( $q, 'query' );
		if( $res === false )
			if (bc::enabled()) bc::error( self::$link ->error, 'error' );
		self::$stats['execs_Time'] += microtime(1)-$_z;
		self::$stats['execs_Count'] ++;
		if (bc::enabled()) bc::log( sprintf( '%9.4fms', 1000*(microtime(1)-$_z) ), 'elapsed' );
		$r = ( self::$link ->affected_rows != 1 || !self::$link ->insert_id ) ? self::$link ->affected_rows : self::$link ->insert_id;
		if (bc::enabled()) bc::log( $r, 'result' );
		if (bc::enabled()) bc::group_End( );
		return $r;
	}
	
	/**
	 * query
	 *
	 * @access public
	 * @param string $query строка запроса
	 * @return mysqli_result результат выполнения запроса
	 */
	public function query( $q )
	{
		if (bc::enabled()) bc::group( 'query['.self::$stats['queries_Count'].']'/*, array( 'Collapsed'=>false, 'Color' => '#194' )*/ );
		$_z = microtime(1);
		$res = self::$link ->query( $q );
		if($q!=='SELECT FOUND_ROWS();')
		$this ->last_query = $q;
		if (bc::enabled()) bc::log( $q, 'query' );
		if( $res === false )
		{
			if (bc::enabled()) bc::error( self::$link ->error, 'error' );
			if (bc::enabled()) bc::group_End( );
			throw new Exception( self::$link ->error );
		}
		self::$stats['queries_Time'] += microtime(1)-$_z;
		self::$stats['queries_Count'] ++;
		if (bc::enabled()) bc::log( sprintf( '%9.4fms', 1000*(microtime(1)-$_z) ), 'elapsed' );
		//if( array_key_exists( 'id', $res ) ) if (bc::enabled()) bc::log( $res, 'result' ); else { if (bc::enabled()) bc::log( reset( $res ), 'row[0]' ); if (bc::enabled()) bc::log( count( $res ), 'rows' ); }
		if (bc::enabled()) bc::group_End( );
		return $res;
	}
	
	public function _value( $q )
	{
		$_r = $this ->_row( $q );
		return is_array( $_r ) ? reset( $_r ) : null;
	}
	
	public function _row( $q )
	{
		$result = $this ->query( $q );
		$_r = $result ->fetch_assoc( );
		$result ->free_result( );
		return $_r;
	}
	
	public function _list( $q, $k = null, $kv = null )
	{
		$_r = array( );
		
		$result = $this ->query( $q );
		
		$z = $result ->fetch_assoc( );
		$k = ( !is_null($k) && isset($z[$k]) ) ? $k : null;
		$kv = ( !is_null($kv) && isset($z[$kv]) ) ? $kv : null;
		
		if( $z )
		{
		do {
			$_v = !is_null( $kv ) ? $z[ $kv ] : $z;
			if( !is_null( $k ) ) $_r[ $z[$k] ] = $_v; else $_r[ ] = $_v;
		} while( $z = $result ->fetch_assoc( ) );
		}
		
		$result ->free_result( );

		if ($kv == 'id')
			$_r = array_map('intval', $_r);

		return $_r;
	}
	
	public function esc( $v ) { return $this ->escape( $v ); }
	public function escape( $v )
	{
		if( !is_array( $v ) ) return self::$link ->escape_string( $v );
		foreach( array_keys( $v ) as $k ) $v[$k] = self::$link ->escape_string( $v[$k] ); return $v;
	}
	
	
	/**
	 * Utilites
	 *
	 */
	public function make_set_list( $a, $d = ',' )
	{
		if( !is_array( $a ) ) return;
		
		//if (bc::enabled()) bc::group('db::make_set_list',array('Collapsed'=>true,'Color'=>'#39f'));
		$d = (string)($d);
		$r = '';
		
		//if (bc::enabled()) bc::log( $a, 'in: '.$d );
		
		foreach( $a as $k => $v )
			$r .= $this ->make_set_list_item( $k, $v, $d );
		$r = mb_substr( $r, 0, -mb_strlen($d.'  ') );
		
		//if (bc::enabled()) bc::log( $r, 'out' );
		//if (bc::enabled()) bc::log( $r, 'result' );
		//if (bc::enabled()) bc::group_End( );
		return $r;
	}

	public function make_set_list_item( $k, $v, $d = ',' )
	{
		// $d == ',' is update/insert query or anything important-less. else is a condition like A OR B OR C, or like A AND B AND C
		$mode = ($d==',') ? 'list' : 'condition';
		$op = substr( $k, -1, 1 ); $op = strpos( '~/<>^!+-@.', $op ) === false ? '=' : $op;
		$k = rtrim( $k, $op );

		//if (bc::enabled()) bc::log( array( $k, $v, $mode, $op ), 'db.it' );
		
		if( $mode == 'list' )
		{
		return
			(is_null($v)		? sprintf( "`%s` = null", $k )
			:($op==='.'			? sprintf( '`%1$s` = concat( %1$s, "%2$s")', $k, $this ->escape( is_array( $v ) ? join( ',', $v ) : $v ) )
			:($op==='='
				? (is_int($v)	? sprintf( '`%s` = %d', $k, $v )
								: sprintf( "`%s` = '%s'", $k, $this ->escape( is_array( $v ) ? join( ',', $v ) : $v ) ) )
								: sprintf( '`%1$s` = `%1$s` %2$s (%3$d)', $k, $op, $v )
			)))." {$d} ";
		}
		
		return
			(($op==='~')	? sprintf( '`%s` like "%3$s%2$s%3$s" ', $k, $this ->escape( $v ), strpos( $v, '%' ) !== false ? '' : '%' )
			:(($op==='/')	? sprintf( "`%s` regexp '%s' ", $k, $this ->escape( $v ) )
			:(is_null($v)	? sprintf( 'isnull( `%s` )', $k )
			:(($op==='^')	? sprintf( "`%s` between '%s' AND '%s'", $k, $this ->escape( $v[0] ), $this ->escape( $v[1] ) )
			:(($op==='!' || $op==='+' || $op==='-')
							? sprintf( '`%s` %s= "%s"', $k, $op, $this ->escape( $v ) )
			:(($op==='@')	? sprintf( '`%s` in (%s)', $k, rtrim($v," \t;") )
			:(is_array($v)	? sprintf( "`%s` in (%s)", $k, '"'.join( '", "', $this ->escape( $v ) ).'"' )
					: sprintf( '`%s` %s "%s"', $k, $op, $this ->escape( $v ) )
			)))))))." {$d} ";
	}

	public function make_insert_values_keys( $a )
	{
		$keys = array( );
		foreach( array_keys( $a ) as $k )
			$keys = array_merge( $keys, array_diff( array_keys( $a[$k] ), $keys ) );
		return $keys;
	}
	
	public function make_insert_values_list( $a, $empty_row = null )
	{
		if( !is_array($a) || empty($a) )
			return;

		if( !is_list_of_maps($a) ) {
			if( !is_map($a) )
				return;

			$a = array($a);
		}

		// prepare $empty_row if null
		if( !is_array( $empty_row ) ) {
			$keys = $this ->make_insert_values_keys( $a );
			$empty_row = array_fill_keys( $keys, null );
		}

		// prepare rows
		$rows = array( );
		foreach( $a as $row ) {
			$new_row = $empty_row;
			foreach( array_keys( $empty_row ) as $k )
			{
				$v = _::pick( @$row[$k], $empty_row[$k] );
				$new_row[$k] =
					(  is_null($v) ? "null"
					:( is_int($v) ? intval( $v )
					:( '"'.($this ->escape( is_array( $v ) ? join( ',', $v ) : $v )).'"'
					)));
			}
			$rows[] = $new_row;
		}
		
		$r = '';
		foreach( $rows as $row )
			$r .= '( '.join( ', ', $row ).' ), ';
		$r = substr( $r, 0, -2 );
		
		return $r;
	}

	public function make_insert_values_query( $table, $a, $empty_row = null, $ignore = null )
	{
		if( is_null( $empty_row ) || !is_array( $empty_row ) )
		{
			$keys = $this ->make_insert_values_keys( $a );
			$empty_row = array_fill_keys( $keys, null );
		} else
			$keys = array_keys( $empty_row );
		
		$keys_list = '`'.join( '`, `', $keys ).'`';
		$values_list = $this ->make_insert_values_list( $a, $empty_row );
		$operator = $ignore?($ignore==1?"INSERT IGNORE":"REPLACE"):"INSERT";
		
		$q = "%s INTO %s(%s) VALUES %s;";
		$q = sprintf( $q, $operator, $table, $keys_list, $values_list );

		return $q;
	}






	
	public function get_stats( $full_Time = false )
	{
		if( $full_Time )
			return self::$stats['queries_Time'] + self::$stats['execs_Time'];
		
		if( self::$stats['queries_Count'] == 0 && self::$stats['execs_Count'] == 0 )
			return '0';
		
		if( self::$stats['execs_Count'] == 0 )
			return sprintf( "%.3f<sup>%d</sup>", self::$stats['queries_Time'], self::$stats['queries_Count'] );
		
		return sprintf( "%.3f<sup>%d</sup> [ %.3f<sup>%d</sup> + %.3f<sup>%d</sup> ]",
			self::$stats['queries_Time'] + self::$stats['execs_Time'],
			self::$stats['queries_Count'] + self::$stats['execs_Count'],
			self::$stats['queries_Time'], self::$stats['queries_Count'],
			self::$stats['execs_Time'], self::$stats['execs_Count']
			);
	}
	
	static public function last_query() {
		$T = isset($this) ? $this : self::instance();
		return $T->last_query;
	}

	static public function error() {
		//$T = isset($this) ? $this : self::instance();
		return self::$link->error;
	}

}

